package com.easyUploader.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.common.exception.CustomException;
import com.common.util.StringUtil;
import com.easyUploader.entity.dao.FileStorage;
import com.easyUploader.entity.vo.FileChunkVo;
import com.easyUploader.property.LocalFileProperty;
import com.easyUploader.service.IBucketService;
import com.easyUploader.service.IFileChunkService;
import com.easyUploader.service.IFileService;
import com.easyUploader.service.IFileStorageService;
import com.easyUploader.util.FileUtil;
import com.easyUploader.util.UnsafeUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sun.misc.Unsafe;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;


/**
 * @author zdh
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class LocalFileServiceImpl implements IFileService {
    private final IFileChunkService fileChunkService;
    private final IFileStorageService fileStorageService;
    private final LocalFileProperty localFileProperty;
    private final IBucketService iBucketService;


    @Transactional
    @Override
    public String uploadFile(FileChunkVo fileChunkVo) {
        //安全检查
        fileChunkVo.safeCheck(iBucketService);
        //先检查是否可以进行秒传
        if(fileStorageService.doCheckFileUpLoad(fileChunkVo.getFileIdentifier(),
                localFileProperty.getFileSavePath(fileChunkVo),fileChunkVo.getFileName(),localFileProperty.getFileVisitUrl(fileChunkVo),fileChunkVo.getBucketName())!=null||
                //只有是当前用户自己已经上传过同样的分片,才不需要重新上传,如果是其他用户上传了同样的分片,还是需要重新上传一遍的
                //文件可以共享,分片不行 (暂时不行,后期会考虑优化)
                fileChunkVo.checkChunkUpLoad(fileChunkService)){
            log.info("当前分片采用秒传策略");
            return localFileProperty.getFileVisitUrl(fileChunkVo);
        }
        log.info(       "当前文件名: {}" +
                        ",文件对应的存储桶为: {}"+
                        ",文件的目录路径: {}"+
                        ",文件总大小: {}"+
                        ",分片标号: {}" +
                        ",分片大小: {}" +
                        ",当前分片实际承载数据大小: {}" +
                        ",总分片数: {}"+
                        ",当前文件对应md5校验码: {}"+
                        ",当前分片对应md5校验码: {}"
                ,fileChunkVo.getFileName(),fileChunkVo.getBucketName(),fileChunkVo.getDirectoryPath()==null?" ":fileChunkVo.getDirectoryPath(),fileChunkVo.getTotalSize(),fileChunkVo.getChunkNumber(),
                fileChunkVo.getChunkSize(),fileChunkVo.getDataSize(),fileChunkVo.getTotalChunk(),
                fileChunkVo.getFileIdentifier(),fileChunkVo.getChunkIdentifier());

        //文件没有采用重命名方式防止重复发生,如果出现同一个目录下文件名重复,那么抛出异常
        String wrappedFullFilePath = fileChunkVo.getWrappedFilePath(localFileProperty);

        //如果对应的文件存储表中没有该文件的记录,那么就采取覆盖策略
        if(fileChunkVo.getChunkNumber().equals(1)&&FileUtil.repeatFileCheck(wrappedFullFilePath)&&
                fileStorageService.count(new QueryWrapper<FileStorage>().eq("file_save_path", wrappedFullFilePath))!=0){
            String msg = StringUtil.isNullOrEmpty(fileChunkVo.getDirectoryPath()) ? fileChunkVo.getBucketName() + "桶下" : fileChunkVo.getDirectoryPath() + "目录下";
            throw new CustomException(msg+"出现重复文件名"+fileChunkVo.getFileName(),msg+"出现重复文件名"+fileChunkVo.getFileName());
        }

        // 单文件上传
        if (fileChunkVo.getTotalChunk() == 1) {
            return uploadSingleFile(wrappedFullFilePath, fileChunkVo);
        }

        // 分片上传，这里使用 uploadFileByRandomAccessFile 方法，也可以使用 uploadFileByMappedByteBuffer 方法上传
        boolean flag = uploadFileByMappedByteBuffer(wrappedFullFilePath, fileChunkVo);

        if (!flag) {
            throw new CustomException("文件上传失败","文件上传失败");
        }

        //保存分片上传信息
        saveFileTrunk(fileChunkVo, wrappedFullFilePath);

        return localFileProperty.getFileVisitUrl(fileChunkVo);
    }

    private void saveFileTrunk(FileChunkVo fileChunkVo, String warppedFullFilePath) {
        fileChunkService.saveFileChunk(fileChunkVo, fileChunkVo1 -> {
            fileStorageService.saveLocalStorage(warppedFullFilePath,fileChunkVo1);
        });
    }


    private boolean uploadFileByRandomAccessFile(String resultFileName, FileChunkVo param) {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "rw")) {
            // 分片大小必须和前端匹配，否则上传会导致文件损坏
            long chunkSize = param.getChunkSize() == 0L ? localFileProperty.getChunkSize() : param.getChunkSize().longValue();
            // 偏移量
            long offset = chunkSize * (param.getChunkNumber() - 1);
            // 定位到该分片的偏移量
            randomAccessFile.seek(offset);
            // 写入
            randomAccessFile.write(param.getFile().getBytes());
        } catch (IOException e) {
            log.error("文件上传失败：" + e);
            return false;
        }
        return true;
    }

    private boolean uploadFileByMappedByteBuffer(String resultFileName, FileChunkVo param) {
        // 分片上传
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "rw");
             FileChannel fileChannel = randomAccessFile.getChannel()) {
            // 分片大小必须和前端匹配，否则上传会导致文件损坏
            long chunkSize = param.getChunkSize() == 0L ? localFileProperty.getChunkSize() : param.getChunkSize().longValue();
            // 写入文件
            long offset = chunkSize * (param.getChunkNumber() - 1);
            byte[] fileBytes = param.getFile().getBytes();
            MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileBytes.length);
            mappedByteBuffer.put(fileBytes);
            // 释放
            unmap(mappedByteBuffer);
            log.info("当前文件保存路径为: {}",resultFileName);
        } catch (IOException e) {
            log.error("文件上传失败：" + e);
            return false;
        }
        return true;
    }

    private String uploadSingleFile(String resultFileName, FileChunkVo fileChunkVo) {
        File saveFile = new File(resultFileName);
        try {
            // 写入
            fileChunkVo.getFile().transferTo(saveFile);
            saveFileTrunk(fileChunkVo,resultFileName);
            log.info("当前文件保存路径为: {}",resultFileName);
        } catch (IOException e) {
            log.error("文件上传失败：" + e);
            throw new CustomException("文件上传失败","文件上传失败");
        }
        return localFileProperty.getFileVisitUrl(fileChunkVo);
    }

    /**
     * 释放 MappedByteBuffer
     * 在 MappedByteBuffer 释放后再对它进行读操作的话就会引发 jvm crash，在并发情况下很容易发生
     * 正在释放时另一个线程正开始读取，于是 crash 就发生了。所以为了系统稳定性释放前一般需要检
     * 查是否还有线程在读或写
     * 来源：https://my.oschina.net/feichexia/blog/212318
     *
     * @param mappedByteBuffer mappedByteBuffer
     */
    public static void unmap(final MappedByteBuffer mappedByteBuffer) {
        Unsafe unsafe = UnsafeUtil.reflectGetUnsafe();
        unsafe.invokeCleaner(mappedByteBuffer);
    }
}